Skip to content

Refresh the UK Universal Credit target contract - #727

Merged
juaristi22 merged 2 commits into
mainfrom
uk-uc-contract-refresh
Aug 20, 2026
Merged

Refresh the UK Universal Credit target contract#727
juaristi22 merged 2 commits into
mainfrom
uk-uc-contract-refresh

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

Summary

Part of #622 and the UC slice of #701. Refreshes the UK national calibration contract from policyengine-uk-data ebf733c to 1.56.16 (12a1e028afeef08d8b2d74ee03fd9de3a78b2dd3), following policyengine-uk-data#452 and chronicle#184.

The contract adds the value-free dwp.uc.households declaration: a prepared UC-receipt indicator summed at benefit-unit level. DWP “household” means the UC unit of assessment, not an ONS household.

This PR deliberately stops after the contract refresh. It does not start #623's calibration step and does not add an active UC reference, for the period-semantics reason below.

Period-semantics receipts and verdict

  • packages/microcosm-build/src/microcosm/build/ledger_targets.py:48-68: each LedgerTargetReference owns an optional per-reference period; periods are not restricted to one file-wide value.
  • packages/microcosm-build/src/microcosm/build/ledger_targets.py:797-845: selector resolution filters to facts at or before that reference's period and fails loudly when no eligible fact exists.
  • packages/microcosm-build/src/microcosm/build/country_spec.py:804-817 and :925-952: target_references.json is loaded and typed into CountrySpec.
  • There is no target_references receipt in packages/microcosm-build/src/microcosm/build/uk_runtime/national_build.py: the national build does not evaluate this resource yet.
  • Adopt the UK national calibration contract (re-homed from chronicle#164 per the #166 ruling) #707's 13 active references all carry period 2023, the certified-spine year.

Verdict: references support per-reference periods in the Belgian convention, but the present UK national build evaluates none of them. A 2025 UC fact cannot resolve for the certified 2023 active subset because the resolver admits only facts at or before the reference period. Labelling the UC row 2023 would fail closed; labelling it 2025 would create an inert mixed-period declaration rather than a target evaluated by the national build. Per the requested stop rule, activation is deferred instead of forcing a misleading period.

Consequently, fixture-backed activation and its loud-failure test are also deferred: adding tests for an activation that the verdict disallows would contradict the stop rule. The Chronicle bundle dependency remains chronicle#184 and a later released consumer feed.

Registry-parity accounting

The governed surface changes from 651 rows (609 mapped + 42 signed exclusions) to 652 rows (610 mapped + 42 signed exclusions):

  • added dwp/uc/householdsdwp.uc.households;
  • no other registry delta surfaced;
  • the four deleted UC jobseeker dispatch names from policyengine-uk-data#452 were never registry rows in the carried contract, so there were no parity entries to remove;
  • obr/jobseekers_allowance remains because it is the distinct published JSA expenditure target, not one of those deleted UC split names.

The exact closure invariant remains 187 declarations = 184 distinct mapped target IDs + 3 signed unmapped declarations.

Tests

All checks passed!
.............................................................            [100%]

Commands:

uv run ruff check packages/microcosm-build/tests/test_uk_national_targets.py
uv run pytest packages/microcosm-build/tests/test_uk_national_targets.py packages/microcosm-build/tests/test_uk_target_references.py packages/microcosm-build/tests/test_ledger_targets.py -q

Scope

No calibration, dataset build, upload, merge, or active-reference change is included. This PR does not close #622; other items remain. The separate #623 claim remains for a later run.

@juaristi22
juaristi22 force-pushed the uk-uc-contract-refresh branch from a68d686 to 051c9ab Compare August 20, 2026 11:33
@juaristi22

Copy link
Copy Markdown
Collaborator

Adversarial review completed and fixes pushed in 051c9aba after rebasing the PR branch onto current main (433c437b).

Findings fixed:

  • The changelog fragment contained the forbidden live-tree policyengine-uk-data string, which was the CI failure in TestUsSources.test_no_incumbent_data_package_references_in_live_tree. Reworded the fragment while preserving the release-note meaning.
  • dwp.uc.households declared a benefit-unit count but bound to value_variable: universal_credit and used the Chronicle-side benefit_unit entity spelling. If activated later, that would sum UC amounts rather than count UC benefit units. Updated it to the existing Microcosm UK benunit entity spelling and benunit_count value variable, and added a focused regression test for the row.

Verification:

  • uv run pytest packages/microcosm-build/tests/test_uk_national_targets.py packages/microcosm-build/tests/test_uk_target_references.py packages/microcosm-build/tests/test_ledger_targets.py packages/microcosm-build/tests/test_us_plan.py::TestUsSources::test_no_incumbent_data_package_references_in_live_tree -q
  • uv run ruff check packages/microcosm-build/tests/test_uk_national_targets.py
  • uv run ruff check packages/microcosm-build/src packages/microcosm-build/tests

GitHub CI is queued on the new head.

@vahid-ahmadi vahid-ahmadi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed as the first link in the chain that closes the UC row on #731, so I checked the seam to chronicle#184 rather than just this diff in isolation. The accounting closes (652 = 610 + 42, 187 targets, 184 mapped), the v1_56_16_refresh note correctly distinguishes "added registry row" from "deleted dispatch names that were never registry rows," and the targeted suites pass for me on the rebased head.

@juaristi22's catch on the binding is the one that mattered — value_variable: universal_credit under a benefit-unit count declaration would have summed UC amounts while the contract said "count units," and it would have done so silently at activation rather than failing. benunit_count + benunit + the focused regression test is right, and the note distinguishing DWP's "household" (UC assessment unit) from an ONS household is exactly the disambiguation uk-data#457 exists over.

One design question before this activates: which month becomes the target value?

chronicle#184 emits eleven monthly benefit-unit counts, and this target's selector pins only source_name + source_concept, so it matches all of them. The resolver's behavior for that case is _latest_period_selector_match — filter to facts at or before the reference period, then take the latest — so the bound value silently becomes a single month's snapshot, whichever month happens to sit closest to the target period, with an ambiguity raise only on ties.

For a stock measure that is defensible, but it should be a declared choice rather than an emergent one, because the two failure modes are opposite and both quiet:

  • If the monthly facts fall at or before the reference period, the target binds one month. UC caseload trends and has seasonality, so "latest month ≤ period" is a materially different number from an annual mean — and #731's UC row is a caseload gap (per-unit entitlement already matches admin to £11.3k), so the number this target binds is precisely the thing being calibrated to.
  • If they all fall after it (monthly DWP releases usually run ahead of a 2023 reference year), _eligible_selector_matches empties and the reference raises "did not match a Ledger fact at or before target period" — a fail-closed refusal, which is the good outcome, but it means activation will surface as an error at #622 wiring rather than here.

Cheapest fix is to make the intent explicit in the declaration — a period pin or a dimension_values pin naming the reference month, or an annual-average concept on the chronicle side if that's the intended basis. Worth settling now: this is the seam where "the target is 6.7m UC units" turns into a specific number, and a silently-chosen month is the kind of thing that gets discovered later as an unexplained calibration residual.

Smaller: the selector uses key source_concept with the value dwp.uc_benefit_units, which is the chronicle fact's measure.concept — its source_concept is dwp.uc_households. This resolves correctly today only because the source_concept key's candidate tuple falls back to the primary concept, so both spellings match. It works, but the pin reads as if it names the source-side spelling when it names the canonical one; pinning dwp.uc_households (or using a concept-named key) would make the intent legible and wouldn't depend on the fallback arm staying tolerant.

Nothing blocking — the contract-side declaration is correct as written, and both points are about what happens when it activates.

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Followed the seam through to chronicle#184's actual periods, which settles which branch of the question above obtains — both of them, in sequence:

The DWP facts span 2025-03 through 2026-02. Against the current UK activation period of 2023 (the membership rule in #707), _eligible_selector_matches filters to facts at or before the reference period and finds none, so the reference raises and the target lands declared but inactive — merging this pair does not bind UC today. That is the fail-closed outcome working, but it means the UC row on #731 stays open after this lands, which is worth saying plainly in the PR body so the chain's next link isn't assumed to be live.

Then at the #723 retarget (FRS 2024-25, calibration year 2025) the same selector goes live and eligible becomes 2025-03…2025-12, so latest-wins binds December 2025 alone as the UC caseload target. That is the specific number that would drive the calibration correcting a −52% caseload gap, chosen by a tie-break rule rather than declared — and December is exactly where you would want to know whether a seasonal peak or trough is being picked up as the annual level.

So the period pin I suggested isn't cosmetic: without it the target is inert now and implicitly December-anchored later, and neither state is visible from the declaration. Same fix as before — pin the reference month explicitly, or add an annual-average concept on the chronicle side if the intended basis is a year rather than a point-in-time stock.

@juaristi22
juaristi22 force-pushed the uk-uc-contract-refresh branch from 051c9ab to 7ff3f16 Compare August 20, 2026 12:00
@juaristi22

Copy link
Copy Markdown
Collaborator

Follow-up: the first post-push CI run exposed one more golden tied to the contract identity digest:

  • test_spec_engine_country_bundles.py still expected the previous UK bundle spec_sha256.
  • Updated it to the compiled digest for this refreshed contract (942ad7df5537e244df7ede7230098e91c617dbdb11e4d0885a94bb3b4f912da5).

Re-verified locally:

  • uv run pytest packages/microcosm-build/tests/test_spec_engine_country_bundles.py::test_country_bundle_loads_once_and_compiles_through_the_shared_core packages/microcosm-build/tests/test_uk_national_targets.py packages/microcosm-build/tests/test_uk_target_references.py packages/microcosm-build/tests/test_ledger_targets.py packages/microcosm-build/tests/test_us_plan.py::TestUsSources::test_no_incumbent_data_package_references_in_live_tree -q
  • uv run ruff check packages/microcosm-build/src packages/microcosm-build/tests

Pushed the amended fix as 7ff3f164.

@juaristi22
juaristi22 merged commit 61869aa into main Aug 20, 2026
4 checks passed
juaristi22 added a commit that referenced this pull request Aug 20, 2026
Rebasing over merged #727 (and #730 beneath it): main's
TestUnevidencedArms now enumerates the closed set of gates that report
evidence_absent when the battery runs unarmed, so the new
uk_calibration_reference_coverage gate joins that set. Also squares the
indentation of the earlier enumeration update in the national-build
terminal-batch test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consume Ledger UK facts: uk_target_references, mapping module, and a compile-parity gate

3 participants